<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <title>IPC之七：使用 System V 共享内存段进行进程间通信的实例 - whowin - 发表我个人原创作品的技术博客</title>
  <meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>

<meta http-equiv="Cache-Control" content="no-transform" />
<meta http-equiv="Cache-Control" content="no-siteapp" />

<meta name="theme-color" content="#f8f5ec" />
<meta name="msapplication-navbutton-color" content="#f8f5ec">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="#f8f5ec">


<meta name="author" content="whowin" /><meta name="description" content="IPC 是 Linux 编程中一个重要的概念，IPC 有多种方式，本文主要介绍共享内存(Shared Memory)，因为没有像管道、消息队列这样的中介介入，所以通常认为共享内存是迄今为止最快的 IPC 方式；Linux 既支持 UNIX SYSTEM V 的共享内存，也支持 POSIX 的共享内存，本文针对 System V 共享内存段，本文给出了多个具体的实例，每个实例均附有完整的源代码；本文所有实例在 Ubuntu 20.04 上编译测试通过，gcc版本号为：9.4.0；本文的实例中涉及多进程编程和信号处理等，阅读本文还需要一些基本的内存管理知识，本文对 Linux 编程的初学者有一些难度。
" /><meta name="keywords" content="linux, socket, hugo, dos" />






<meta name="generator" content="Hugo 0.97.3 with theme even" />


<link rel="canonical" href="https://whowin.gitee.io/post/blog/ipc/0017-systemv-shared-memory/" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">



<link href="/sass/main.min.e3fea119b1980e848b03dffbeddb11dd0fba483eed0e5f11870fb8e31f145bbd.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.1.20/dist/jquery.fancybox.min.css" integrity="sha256-7TyXnr2YU040zfSP+rEcz29ggW4j56/ujTPwjMzyqFY=" crossorigin="anonymous">


<meta property="og:title" content="IPC之七：使用 System V 共享内存段进行进程间通信的实例" />
<meta property="og:description" content="IPC 是 Linux 编程中一个重要的概念，IPC 有多种方式，本文主要介绍共享内存(Shared Memory)，因为没有像管道、消息队列这样的中介介入，所以通常认为共享内存是迄今为止最快的 IPC 方式；Linux 既支持 UNIX SYSTEM V 的共享内存，也支持 POSIX 的共享内存，本文针对 System V 共享内存段，本文给出了多个具体的实例，每个实例均附有完整的源代码；本文所有实例在 Ubuntu 20.04 上编译测试通过，gcc版本号为：9.4.0；本文的实例中涉及多进程编程和信号处理等，阅读本文还需要一些基本的内存管理知识，本文对 Linux 编程的初学者有一些难度。" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://whowin.gitee.io/post/blog/ipc/0017-systemv-shared-memory/" /><meta property="article:section" content="post" />
<meta property="article:published_time" content="2023-09-12T16:43:29+08:00" />
<meta property="article:modified_time" content="2023-09-12T16:43:29+08:00" />

<meta itemprop="name" content="IPC之七：使用 System V 共享内存段进行进程间通信的实例">
<meta itemprop="description" content="IPC 是 Linux 编程中一个重要的概念，IPC 有多种方式，本文主要介绍共享内存(Shared Memory)，因为没有像管道、消息队列这样的中介介入，所以通常认为共享内存是迄今为止最快的 IPC 方式；Linux 既支持 UNIX SYSTEM V 的共享内存，也支持 POSIX 的共享内存，本文针对 System V 共享内存段，本文给出了多个具体的实例，每个实例均附有完整的源代码；本文所有实例在 Ubuntu 20.04 上编译测试通过，gcc版本号为：9.4.0；本文的实例中涉及多进程编程和信号处理等，阅读本文还需要一些基本的内存管理知识，本文对 Linux 编程的初学者有一些难度。"><meta itemprop="datePublished" content="2023-09-12T16:43:29+08:00" />
<meta itemprop="dateModified" content="2023-09-12T16:43:29+08:00" />
<meta itemprop="wordCount" content="5996">
<meta itemprop="keywords" content="Linux,进程间通信,IPC,POSIX,共享内存,Shared Memory," /><meta name="twitter:card" content="summary"/>
<meta name="twitter:title" content="IPC之七：使用 System V 共享内存段进行进程间通信的实例"/>
<meta name="twitter:description" content="IPC 是 Linux 编程中一个重要的概念，IPC 有多种方式，本文主要介绍共享内存(Shared Memory)，因为没有像管道、消息队列这样的中介介入，所以通常认为共享内存是迄今为止最快的 IPC 方式；Linux 既支持 UNIX SYSTEM V 的共享内存，也支持 POSIX 的共享内存，本文针对 System V 共享内存段，本文给出了多个具体的实例，每个实例均附有完整的源代码；本文所有实例在 Ubuntu 20.04 上编译测试通过，gcc版本号为：9.4.0；本文的实例中涉及多进程编程和信号处理等，阅读本文还需要一些基本的内存管理知识，本文对 Linux 编程的初学者有一些难度。"/>

<!--[if lte IE 9]>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/classlist/1.1.20170427/classList.min.js"></script>
<![endif]-->

<!--[if lt IE 9]>
  <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
<![endif]-->

  <script async src="/js/busuanzi.pure.mini.js"></script><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-9724909319263152"
     crossorigin="anonymous"></script>


</head>
<body>
  <div id="mobile-navbar" class="mobile-navbar">
  <div class="mobile-header-logo">
    <a href="/" class="logo">WhoWin</a>
  </div>
  <div class="mobile-navbar-icon">
    <span></span>
    <span></span>
    <span></span>
  </div>
</div>
<nav id="mobile-menu" class="mobile-menu slideout-menu">
  <ul class="mobile-menu-list">
    <a href="/">
        <li class="mobile-menu-item">首页</li>
      </a><a href="/post/">
        <li class="mobile-menu-item">文章归档</li>
      </a><a href="/article-categories/categories/">
        <li class="mobile-menu-item">文章分类</li>
      </a><a href="/tags/">
        <li class="mobile-menu-item">文章标签</li>
      </a><a href="/about/about/">
        <li class="mobile-menu-item">关于</li>
      </a>
  </ul>

  


</nav>

  <div class="container" id="mobile-panel">
    <header id="header" class="header">
        <div class="logo-wrapper">
  <a href="/" class="logo">WhoWin</a>
  
  <div style="position:absolute; left: 80px; top: 75px; color: crimson">
      ———开源和分享是技术发展的源泉和动力；本博客所有文章均为原创
  </div>
</div>





<nav class="site-navbar">
  <ul id="menu" class="menu">
    <li class="menu-item">
        <a class="menu-item-link" href="/">首页</a>
      </li><li class="menu-item">
        <a class="menu-item-link" href="/post/">文章归档</a>
      </li><li class="menu-item">
        <a class="menu-item-link" href="/article-categories/categories/">文章分类</a>
      </li><li class="menu-item">
        <a class="menu-item-link" href="/tags/">文章标签</a>
      </li><li class="menu-item">
        <a class="menu-item-link" href="/about/about/">关于</a>
      </li>
  </ul>
</nav>

    </header>

    <main id="main" class="main">
      <div class="content-wrapper">
        <div id="content" class="content">
          <article class="post">
    
    <header class="post-header">
      <h1 class="post-title">IPC之七：使用 System V 共享内存段进行进程间通信的实例</h1>

      <div class="post-meta">
        <span class="post-time"> 2023-09-12 </span>
        <div class="post-category">
            <a href="/categories/ipc/"> IPC </a>
            <a href="/categories/linux/"> Linux </a>
            <a href="/categories/c-language/"> C Language </a>
            </div>
        
      </div>
    </header>

    <div class="post-toc" id="post-toc">
  <h2 class="post-toc-title">文章目录</h2>
  <div class="post-toc-content always-active">
    <nav id="TableOfContents">
  <ul>
    <li>
      <ul>
        <li><a href="#1-system-v-共享内存基本概念">1 System V 共享内存基本概念</a></li>
        <li><a href="#2-创建打开共享内存段">2 创建/打开共享内存段</a></li>
        <li><a href="#3-映射共享内存段地址到进程地址空间">3 映射共享内存段地址到进程地址空间</a></li>
        <li><a href="#4-共享内存的控制操作">4 共享内存的控制操作</a></li>
        <li><a href="#5-实例">5 实例</a></li>
        <li><a href="#6-操作共享内存段的命令行命令">6 操作共享内存段的命令行命令</a></li>
        <li><a href="#欢迎订阅-进程间通信专栏httpsblogcsdnnetwhowincategory_12404164html"><strong>欢迎订阅 <a href="https://blog.csdn.net/whowin/category_12404164.html">『进程间通信专栏』</a></strong></a></li>
      </ul>
    </li>
  </ul>
</nav>
  </div>
</div>
    <div class="post-content">
      <p>IPC 是 Linux 编程中一个重要的概念，IPC 有多种方式，本文主要介绍共享内存(Shared Memory)，因为没有像管道、消息队列这样的中介介入，所以通常认为共享内存是迄今为止最快的 IPC 方式；Linux 既支持 UNIX SYSTEM V 的共享内存，也支持 POSIX 的共享内存，本文针对 System V 共享内存段，本文给出了多个具体的实例，每个实例均附有完整的源代码；本文所有实例在 Ubuntu 20.04 上编译测试通过，gcc版本号为：9.4.0；本文的实例中涉及多进程编程和信号处理等，阅读本文还需要一些基本的内存管理知识，本文对 Linux 编程的初学者有一些难度。</p>
<h2 id="1-system-v-共享内存基本概念">1 System V 共享内存基本概念</h2>
<ul>
<li>
<p>共享内存实际上是一个内存区域(段)的映射，一个进程创建一个共享内存段，然后被其它进程映射到自身的地址空间上，从而实现共享，各个进程实际读取的是同一个内存区域；</p>
</li>
<li>
<p>之所以说共享内存是迄今为止速度最快的 IPC 方式，是因为多个进程不管是使用 FIFO 还是消息队列传递数据，都需要经过内核的中转，而共享内存则不需要；</p>
</li>
<li>
<p>考虑如下过程：进程 A 从文件中读出数据，并将数据传递给进程 B，进程 B 再将文件内容写入到另一个文件；</p>
</li>
<li>
<p>当使用 FIFO 或者消息队列传递数据时，过程如下：</p>
<ul>
<li>进程 A 首先要将文件内容读出并存储到自身地址空间的内存中;</li>
<li>进程 A 将数据发送到 FIFO 或者消息队列实际上是将自身地址空间的数据拷贝到内核空间的内存中；</li>
<li>进程 B 从 FIFO 或者消息队列中接收数据实际上是从内核空间的内存中将数据拷贝到自身地址空间的内存中；</li>
<li>进程 B 将自身地址空间内存中的数据写入文件。</li>
</ul>
</li>
<li>
<p>当使用共享内存传递数据时，过程如下：</p>
<ul>
<li>进程 A 从文件中读出数据，并直接存储在共享内存中；</li>
<li>进程 B 将共享内存中的数据写入到另一个文件中；</li>
</ul>
</li>
<li>
<p>由此可见，共享内存在 IPC 上的优势；</p>
</li>
<li>
<p>Linux 既支持 Sytem V 共享内存也支持 POSIX 共享内存，<strong>本文针对 System V 共享内存</strong>；</p>
</li>
<li>
<p>以下如无特别说明，共享内存均指 System V 共享内存；</p>
</li>
<li>
<p>使用共享内存有如下一些限制：</p>
<ul>
<li><strong>SHMMNI</strong>：整个系统中，共享内存段的最大数；</li>
<li><strong>SHMALL</strong>：整个系统中，共享内存的总长度(字节数)；</li>
<li><strong>SHMMAX</strong>：每个共享内存段的最大长度(字节数)；</li>
<li><strong>SHMMIN</strong>：共享内存段的最小长度(字节数)；</li>
</ul>
</li>
<li>
<p>可以使用命令行命令 <code>ipcs -m -l</code> 查看这些限制值；</p>
<p><img src="https://whowin.gitee.io/images/190017/screen-of-ipcs-m-l.png" alt="screenshot of &amp;lsquo;ipcs -m -l&amp;rsquo;"></p>
</li>
<li>
<p>也可以在 proc 文件系统中找到这些限制值</p>
<p><img src="https://whowin.gitee.io/images/190017/screenshot-max-proc.png" alt="screenshot of proc"></p>
</li>
<li>
<p>还可以使用命令 <code>sysctl kernel.shm{mni,all,max}</code> 查看这些限制值；</p>
<p><img src="https://whowin.gitee.io/images/190017/screenshot-of-sysctl.png" alt="screenshot of sysctl"></p>
</li>
</ul>
<hr>
<h2 id="2-创建打开共享内存段">2 创建/打开共享内存段</h2>
<ul>
<li>
<p>和 System V 消息队列和信号量集一样，共享内存段也是使用 key_t 和 ID 进行标识，请参考文章<a href="https://whowin.gitee.io/post/blog/ipc/0013-systemv-message-queue/">《IPC之三：使用 System V 消息队列进行进程间通信的实例》</a>中关于 key 和 ID 的介绍；</p>
</li>
<li>
<p>key 一般使用 ftok() 生成；</p>
</li>
<li>
<p>在使用共享内存前，需要创建一个新的共享内存段，或者打开一个已经存在的共享内存段；</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-C" data-lang="C"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/ipc.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/shm.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">shmget</span><span class="p">(</span><span class="n">key_t</span> <span class="n">key</span><span class="p">,</span> <span class="n">size_t</span> <span class="n">size</span><span class="p">,</span> <span class="kt">int</span> <span class="n">shmflg</span><span class="p">);</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>执行成功，shmget() 返回一个与 key 关联的共享内存段的 ID(Identifier)，执行失败则返回 -1，errno 中为错误代码；</p>
</li>
<li>
<p><strong>key</strong>：参考文章<a href="https://whowin.gitee.io/post/blog/ipc/0013-systemv-message-queue/">《IPC之三：使用 System V 消息队列进行进程间通信的实例》</a>中关于 key 的介绍和生成方法；</p>
</li>
<li>
<p><strong>size</strong>：创建的共享内存段的大小，实际分配的内存大小会向上取整到 PAGE_SIZE 的整数倍；</p>
<blockquote>
<p>PAGE_SIZE 是一个内存页的大小，在内核中定义，我们可以使用命令 getconf PAGESIZE 查看其具体值，通常为 4096；</p>
</blockquote>
</li>
<li>
<p><strong>shmflag</strong>：标志位，常用的其实就是 IPC_CREAT 和 IPC_EXCL，还有一些其它的不常用值，就不做介绍了；</p>
<ul>
<li>当 <strong>IPC_CREAT</strong> 时，如果 key 对应的共享内存段存在，则返回其 ID，如果 key 对应的共享内存段不存在，则建立一个新的与 key 关联的共享内存段，并返回其 ID；</li>
<li>当 <strong>IPC_CREAT | IPC_EXCL</strong> 时，如果 key 对应的共享内存段存在，则报错返回 -1，<code>errno = EEXIST(File exists)</code>；如果 key 对应的共享内存段不存在，则建立一个新的与 key 关联的共享内存段，并返回其 ID；</li>
<li>当 <strong>IPC_EXEL</strong> 时，如果 key 对应的共享内存段存在，则返回其 ID(这点和 IPC_CREAT 一样)，如果 key 对应的共享内存段不存在，则返回 -1，<code>errno = ENOENT(No such file or directory)</code>；</li>
<li>另外，shmflag 还可以加上所创建的共享内存段的读写权限，要用八进制表示，比如：0666；</li>
<li>shmflag 举例：<code>IPC_CREAT | IPC_EXEL | 0666</code></li>
</ul>
</li>
<li>
<p>当 <code>key = IPC_PRIVATE</code> 时，<code>shmget()</code> 将创建一个新的共享内存段并返回其 ID；</p>
<ul>
<li>这样生成的共享内存段只有 ID，没有 key(key 为 0)，所以其它进程并不能方便地使用这个共享内存段，通常只能在子进程之间使用；</li>
<li>实际上，IPC_PRIVATE 的值是 0，所以我们自己生成的 key 不能是 0，否则相当于将 key 设置为 IPC_PRIVATE；</li>
</ul>
</li>
</ul>
<hr>
<h2 id="3-映射共享内存段地址到进程地址空间">3 映射共享内存段地址到进程地址空间</h2>
<ul>
<li>已经获取了 ID 的共享内存段，需要将其地址映射到当前进程的地址空间上才能正常使用，这个过程使用 <code>shmat()</code> 实现；
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-C" data-lang="C"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/types.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/shm.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="o">*</span><span class="nf">shmat</span><span class="p">(</span><span class="kt">int</span> <span class="n">shmid</span><span class="p">,</span> <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">shmaddr</span><span class="p">,</span> <span class="kt">int</span> <span class="n">shmflg</span><span class="p">);</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>调用成功，shmat() 返回共享内存段在当前进程的地址空间的映射地址，失败则返回 <code>(void *) -1</code>，errno 中为错误代码；</li>
<li><strong>shmid</strong>：使用 shmget() 获得的共享内存段的 ID;</li>
<li><strong>shmaddr</strong>：映射共享内存段的用户进程地址空间的地址；
<ul>
<li>如果 <code>shmaddr</code> 为 NULL，系统会选择一个未使用的、页对齐的地址作为共享内存段的映射地址；</li>
<li>如果 <code>shmaddr</code> 不为 NULL，并且 <code>shmflg</code> 中指定了 <code>SHM_RND</code>，系统会把共享内存段映射到 <code>shmaddr</code> 地址向下的最近的内存页的整数倍的地址上；</li>
<li>其它 <code>shmaddr</code> 不为 NULL 的情况，<code>shmaddr</code> 必须是一个页对齐的地址；</li>
<li>大多数情况下，<strong>shmaddr</strong> 填 NULL 即可，由系统为我们选择的映射地址通常是安全、可靠的；</li>
</ul>
</li>
<li><strong>shmflag</strong>：这个标志的值除了前面提到的 SHM_RND 外，还可以有下列几个：
<ul>
<li>SHM_EXEC：如果进程对共享内存段有执行权限，允许执行共享内存段中的代码；</li>
<li>SHM_RDONLY：以只读方式映射共享内存段，不指定该标志，映射的共享内存段是有读写权限的，没有只写权限的共享内存段的概念；</li>
<li>SHM_REMAP：重新映射共享内存段到 <code>shmaddr</code> 指定的地址上，此时，<code>shmaddr</code> 不能为空，<code>shmaddr</code> 向上至共享内存段长度之间如果已有其它映射，将导致一个 EINVAL 错误；</li>
<li>大多数情况下，我们并不需要设置 <code>shmflag</code>，将其设为 0 即可；</li>
</ul>
</li>
<li>实际上 <code>shmat()</code> 最常用的方式就是：<code>void *ptr = shmat(shmid, NULL, 0);</code>；</li>
<li>共享内存在使用完后要释放这个映射地址，否则，这块地址是没有办法再次使用的，释放映射地址使用 <code>shmdt()</code>；
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-C" data-lang="C"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/types.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/shm.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">shmdt</span><span class="p">(</span><span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">shmaddr</span><span class="p">);</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>调用成功返回 0，调用失败返回 -1，errno 为错误代码；</li>
<li><strong>shmaddr</strong> 为共享内存段的映射地址。</li>
</ul>
<hr>
<h2 id="4-共享内存的控制操作">4 共享内存的控制操作</h2>
<ul>
<li>
<p>对共享内存的控制操作使用 <code>shmctl()</code></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-C" data-lang="C"><span class="line"><span class="cl"><span class="k">struct</span> <span class="n">ipc_perm</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">key_t</span>          <span class="n">__key</span><span class="p">;</span>       <span class="cm">/* Key supplied to shmget(2) */</span>
</span></span><span class="line"><span class="cl">    <span class="n">uid_t</span>          <span class="n">uid</span><span class="p">;</span>         <span class="cm">/* Effective UID of owner */</span>
</span></span><span class="line"><span class="cl">    <span class="n">gid_t</span>          <span class="n">gid</span><span class="p">;</span>         <span class="cm">/* Effective GID of owner */</span>
</span></span><span class="line"><span class="cl">    <span class="n">uid_t</span>          <span class="n">cuid</span><span class="p">;</span>        <span class="cm">/* Effective UID of creator */</span>
</span></span><span class="line"><span class="cl">    <span class="n">gid_t</span>          <span class="n">cgid</span><span class="p">;</span>        <span class="cm">/* Effective GID of creator */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">short</span> <span class="n">mode</span><span class="p">;</span>        <span class="cm">/* Permissions + SHM_DEST and SHM_LOCKED flags */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">short</span> <span class="n">__seq</span><span class="p">;</span>       <span class="cm">/* Sequence number */</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">struct</span> <span class="n">shmid_ds</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">ipc_perm</span> <span class="n">shm_perm</span><span class="p">;</span>    <span class="cm">/* Ownership and permissions */</span>
</span></span><span class="line"><span class="cl">    <span class="n">size_t</span>          <span class="n">shm_segsz</span><span class="p">;</span>   <span class="cm">/* Size of segment (bytes) */</span>
</span></span><span class="line"><span class="cl">    <span class="n">time_t</span>          <span class="n">shm_atime</span><span class="p">;</span>   <span class="cm">/* Last attach time */</span>
</span></span><span class="line"><span class="cl">    <span class="n">time_t</span>          <span class="n">shm_dtime</span><span class="p">;</span>   <span class="cm">/* Last detach time */</span>
</span></span><span class="line"><span class="cl">    <span class="n">time_t</span>          <span class="n">shm_ctime</span><span class="p">;</span>   <span class="cm">/* Last change time */</span>
</span></span><span class="line"><span class="cl">    <span class="n">pid_t</span>           <span class="n">shm_cpid</span><span class="p">;</span>    <span class="cm">/* PID of creator */</span>
</span></span><span class="line"><span class="cl">    <span class="n">pid_t</span>           <span class="n">shm_lpid</span><span class="p">;</span>    <span class="cm">/* PID of last shmat(2)/shmdt(2) */</span>
</span></span><span class="line"><span class="cl">    <span class="n">shmatt_t</span>        <span class="n">shm_nattch</span><span class="p">;</span>  <span class="cm">/* No. of current attaches */</span>
</span></span><span class="line"><span class="cl">    <span class="p">...</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/ipc.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/shm.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">shmctl</span><span class="p">(</span><span class="kt">int</span> <span class="n">shmid</span><span class="p">,</span> <span class="kt">int</span> <span class="n">cmd</span><span class="p">,</span> <span class="k">struct</span> <span class="n">shmid_ds</span> <span class="o">*</span><span class="n">buf</span><span class="p">);</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>参考文章<a href="https://whowin.gitee.io/post/blog/ipc/0013-systemv-message-queue/">《IPC之三：使用 System V 消息队列进行进程间通信的实例》</a>，其调用方法非常接近；</p>
</li>
<li>
<p><code>shmctl()</code> 在 <strong>shmid</strong> 所指定的共享内存段上执行一个由 cmd 指定的操作，包括：获取/设置共享内存段的属性，删除共享内存段等；</p>
</li>
<li>
<p>Linux 下允许的 cmd 值为：</p>
<ul>
<li>
<p><strong>IPC_STAT</strong>：获取共享内存段的属性，此时，buf 指向一个 <code>struct shmid_ds</code>，共享内存段的属性将从内核空间拷贝到 buf 中；</p>
</li>
<li>
<p><strong>IPC_SET</strong>：设置共享内存段的属性，实践中主要用来设置共享内存段的读写权限，已知的可以设置的字段有：<code>shm_perm.uid</code>、<code>shm_perm.gid</code> 以及 <code>shm_perm.mode</code>(低9位)；</p>
<blockquote>
<p>具体编程实践中，往往是先使用 <strong>IPC_STAT</strong> 命令获取共享内存段的属性，然后修改需要设置的值，再使用 <strong>IPC_SET</strong> 命令设置属性；</p>
</blockquote>
</li>
<li>
<p><strong>IPC_RMID</strong>：删除一个共享内存段，实际上，执行完这个命令后，共享内存段不一定立即被销毁，只有当所有映射了共享内存段地址的进程全部释放了映射地址后，这个共享内存段才会被真正销毁掉；</p>
<blockquote>
<p>如果已经执行了 IPC_RMID 命令，但共享内存段还没有被销毁，这时使用 IPC_STAT 获取属性时，会看到 shm_perm.mode 字段上被写入了 SHM_DEST 标记；</p>
</blockquote>
<blockquote>
<p>必须要保证调用 IPC_RMID 后共享内存最终会被销毁，否则其残留将一直驻留在内存或交换区中。</p>
</blockquote>
</li>
<li>
<p><strong>SHM_INFO</strong>：获取有关共享内存所消耗的系统资源的信息，这些信息将返回到一个 <code>struct shm_info</code> 中，此时，buf 要指向一个 <code>struct shm_info</code>；</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-C" data-lang="C"><span class="line"><span class="cl"><span class="k">struct</span> <span class="n">shm_info</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int</span>           <span class="n">used_ids</span><span class="p">;</span>         <span class="cm">/* # of currently existing segments */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">shm_tot</span><span class="p">;</span>          <span class="cm">/* Total number of shared memory pages */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">shm_rss</span><span class="p">;</span>          <span class="cm">/* # of resident shared memory pages */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">shm_swp</span><span class="p">;</span>          <span class="cm">/* # of swapped shared memory pages */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">swap_attempts</span><span class="p">;</span>    <span class="cm">/* Unused since Linux 2.4 */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">swap_successes</span><span class="p">;</span>   <span class="cm">/* Unused since Linux 2.4 */</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></td></tr></table>
</div>
</div><blockquote>
<p>获得的信息包括：当前已存在的共享内存段的数量、共享内存页的总数量、驻留在物理内存的共享内存页的数量以及在交换区的共享内存页的数量；对一般的应用程序而言，这些值基本没有什么用处。</p>
</blockquote>
</li>
<li>
<p><strong>SHM_STAT</strong> 和 <strong>SHM_STAT_ANY</strong> 基本用不上，这里就不介绍了；</p>
</li>
<li>
<p><strong>SHM_LOCK</strong>：程序可以使用这个命令&quot;锁定&quot;共享内存段，使其不会被放在交换区(让这个共享内存段一直驻留在物理内存中)，被&quot;锁定&quot;的共享内存段，当使用 <strong>IPC_STAT</strong> 获取其属性时，会在 <code>shm_perm.mode</code> 字段上看到 <strong>SHM_LOCKED</strong> 标志；</p>
</li>
</ul>
</li>
<li>
<p><strong>源程序</strong>：<a href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/190017/shm-ctl.c">shm-ctl.c</a>(<strong>点击文件名下载源程序</strong>)演示了如何使用 shmctl() 对共享内存段进行操作；</p>
</li>
<li>
<p>编译：<code>gcc -Wall shm-ctl.c -o shm-ctl</code></p>
</li>
<li>
<p>运行：<code>./shm-ctl</code></p>
<ol>
<li>该程序首先建立了一个共享内存段，并返回其 ID；</li>
<li>使用 <code>IPC_STAT</code> 获取了共享内存段的属性，并显示了其中的读/写权限，可以看到和建立共享内存段时设定的权限一样；</li>
<li>将共享内存段的读/写权限的最低 3 位清 0，然后使用 <code>IPC_SET</code> 重新设置其读/写权限；</li>
<li>使用 <code>IPC_STAT</code> 获取了共享内存段的属性，并显示了其中的读/写权限，可以看到和修改过的权限一样；</li>
<li>显示属性中的 <strong>LOCKED</strong> 标志，可以看到没有 LOCKED 标志；</li>
<li>使用 <code>SHM_INFO</code> 读取共享内存段的信息，并显示出来；</li>
<li>使用 <code>SHM_LOCK</code> &lsquo;锁定&rsquo;共享内存段；</li>
<li>使用 <code>IPC_STAT</code> 获取了共享内存段的属性，并显示 LOCKED 标志，可以看到 LOCKED 标志已经被设置；</li>
<li>使用 <code>SHM_UNLOCK</code> &lsquo;解锁&rsquo;共享内存段；</li>
<li>销毁共享内存段。</li>
</ol>
</li>
<li>
<p>运行截图：</p>
<p><img src="https://whowin.gitee.io/images/190017/screenshot-of-shm-ctl.png" alt="Screenshot of running shm-ctl"></p>
</li>
</ul>
<hr>
<h2 id="5-实例">5 实例</h2>
<ul>
<li>
<p>这个实例是一个服务端，若干个客户端使用共享内存段的例子</p>
</li>
<li>
<p>服务端源程序：<a href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/190017/shm-server.c">shm-server.c</a>(<strong>点击文件名下载源程序</strong>)</p>
</li>
<li>
<p>客户端源程序：<a href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/190017/shm-client.c">shm-client.c</a>(<strong>点击文件名下载源程序</strong>)</p>
</li>
<li>
<p>包含文件：<a href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/190017/shm-public.h">shm-public.h</a>(<strong>点击文件名下载源程序</strong>)</p>
</li>
<li>
<p>基本过程：</p>
<blockquote>
<p>客户端程序 fork 出若干个(本例中为 5 个，由宏 MAX_PROCESSES 控制子进程数量)子进程，每个子进程运行相同的程序，子进程会随机从字符串数组中选择一个字符串，将其依次放到共享内存中，当共享内存满时则停止放入；</p>
</blockquote>
<blockquote>
<p>服务端程序从共享内存中依次取出客户端放入的字符串(先进先出原则)，并将其打印到屏幕上，当共享内存空时则停止取出字符串；</p>
</blockquote>
</li>
<li>
<p>服务端程序说明：</p>
<ol>
<li>建立共享内存段，并将其映射到服务端进程的地址空间，共享内存是由一个结构体(<code>struct shared_memory</code>)组成；
<ul>
<li>结构体中有一个可以存储若干(本例中为 10，由宏 <code>MAX_BUFFERS</code> 决定)个字符串的缓冲区(<code>buf[MAX_BUFFERS][MAX_STR_LEN]</code>)；</li>
<li>结构体中有一个字符串索引号(<code>string_index</code>)，用于向缓冲区(<code>buf</code>)中放入字符串，初始值为 0，放入一个字符串后 +1，达到 <code>MAX_BUFFERS</code> 时归 0；</li>
<li>结构体中有一个打印索引号(<code>print_index</code>)，用于从缓冲区(<code>buf</code>)中取出字符串并打印到屏幕上，初始值为 0，取出一个字符串后 +1，达到 <code>MAX_BUFFERS</code> 时归 0；</li>
</ul>
</li>
<li>建立信号量集，里面包含三个信号量：
<ul>
<li>SEM_MUTEX：这是一个互斥信号量，用于访问临界区(对共享内存进行存取的代码)的互斥，初始值为 1；</li>
<li>SEM_BUF_FULL：这是一个计数信号量，其初始值为 <code>MAX_BUFFERS</code>，客户端程序放入一个字符串后 <code>SEM_BUF_FULL-1</code>，服务端程序取出一个字符串后 <code>SEM_BUF_FULL+1</code>，为 0 时，表示共享内存已满，无法再向共享内存中放入字符串；</li>
<li>SEM_BUF_EMPTY：这是一个计数信号量，其初始值为 0，服务端程序取出一个字符串后 <code>SEM_BUF_EMPTY-1</code>，客户端程序放入一个字符串后 <code>SEM_BUF_EMPTY+1</code>，为 0 时，表示共享内存空，无法再从共享内存中取出字符串；</li>
</ul>
</li>
<li>对计数信号量 SEM_BUF_EMPTY 做 P 操作，即：<code>SEM_BUF_EMPTY-1</code>，如果获取到信号量(<code>SEM_BUF_EMPTY&gt;0</code>)，表示共享内存中有没有处理的字符串；</li>
<li>使用打印索引号(<code>print_index</code>)从共享内存中读出一个字符串(<code>buf[print_index]</code>)并显示在屏幕上，打印索引号(<code>print_index</code>) +1；</li>
<li>对计数信号量 SEM_BUF_FULL 做 V 操作，即：<code>SEM_BUF_FULL+1</code>；</li>
<li>循环 step 3</li>
</ol>
<blockquote>
<p>由于只有一个服务端程序运行，服务端程序仅对打印索引号(<code>print_index</code>)进行了写操作，而客户端程序无需对打印索引号(<code>print_index</code>)进行操作，所以这里没有请求互斥信号量(<code>SEM_MUTEX</code>)，对本例而言没有风险，如果有多个服务端，应在 <code>step 4</code> 前请求互斥信号量(SEM_MUTEX)，在 <code>step 5</code> 后释放互斥信号量(SEM_MUTEX)；如果增加了互斥信号量的请求和释放，则服务端程序在从共享内存中取出字符串的过程中，客户端程序是无法将新字符串放入共享内存的；本例中的做法可以使服务端程序取出字符串时，客户端程序同时可以将新字符串放入共享内存，运行效率略高一些；因此，合理地运用互斥信号量可以有效地提高整体的运行效率。</p>
</blockquote>
</li>
<li>
<p>客户端程序说明：</p>
<ol>
<li>获取服务端建立的共享内存段的 ID，并映射到客户端进程的地址空间；</li>
<li>获取服务端建立的信号量集的 ID；</li>
<li>对计数信号量 SEM_BUF_FULL 进行 P 操作，即： <code>SEM_BUF_FULL-1</code>，如果获取到信号量(<code>SEM_BUF_FULL&gt;0</code>)，表示共享内存中有空闲空间放入新字符串，并且当前进程已经抢占了一个位置；</li>
<li>对互斥信号量 SEM_MUTEX 进行 P 操作，获取进入临界区的许可，因为下面要改变字符串索引号(<code>string_index</code>)，有多个客户端进程都要做这个操作，必须保证同时只有一个进程在做这个操作；</li>
<li>向字符串索引号(<code>string_index</code>)所在的共享内存位置(<code>buf[string_index]</code>)放入随机字符串；</li>
<li>字符串索引号(<code>string_index</code>) +1；</li>
<li>对互斥信号量 SEM_MUTEX 执行 V 操作，释放互斥信号量 SEM_MUTEX；</li>
<li>对计数信号量 SEM_BUF_EMPTY 执行 V 操作，即：<code>SEM_BUF_EMPTY+1</code>；</li>
<li>循环至 <code>step 3</code>。</li>
</ol>
</li>
<li>
<p>服务端和客户端程序均只能用 <code>CTRL + C</code> 退出，程序中截获了 <code>CTRL + C</code> 的信号；</p>
</li>
<li>
<p>运行时要打开两个终端窗口，一个终端运行 <code>.\shm-server</code>，另一个终端运行 <code>./shm-client</code>；</p>
</li>
<li>
<p>一定要先运行 <code>shm-server</code>，再运行 <code>shm-client</code>，因为共享内存段和信号量集都是在 <code>shm-server</code> 中建立的，单独运行 <code>shm-client</code> 无法运行成功；</p>
</li>
<li>
<p>运行动图：</p>
<p><img src="https://whowin.gitee.io/images/190017/shm-server-client.gif" alt="GIF for running shm-server and shm-client"></p>
</li>
</ul>
<hr>
<h2 id="6-操作共享内存段的命令行命令">6 操作共享内存段的命令行命令</h2>
<ul>
<li>
<p><code>ipcs -m -l</code> - 显示共享内存段的限制值；</p>
</li>
<li>
<p><code>ipcs -m</code> - 显示现有共享内存端的 key、ID 等部分属性；</p>
</li>
<li>
<p><code>ipcs -m -i &lt;ID&gt;</code> - 显示指定 ID 的共享内存段的属性(比 <code>ipcs -s</code> 显示的属性要多些)；</p>
</li>
<li>
<p><code>ipcrm -M &lt;key&gt;</code> - 删除指定 key 的共享内存段；</p>
</li>
<li>
<p><code>ipcrm -m &lt;ID&gt;</code> - 删除指定 ID 的共享内存段；</p>
</li>
<li>
<p><code>ipcrm --all=shm</code> - 删除所有的共享内存段；</p>
</li>
<li>
<p><code>ipcmk -M &lt;shm size&gt;</code> - 创建一个新的大小为 <code>&lt;shm size&gt;</code> 共享内存段，其读写权限为默认的 0644；</p>
</li>
<li>
<p><code>ipcmk -M &lt;shm size&gt; -p &lt;perm&gt;</code> - 创建一个新的大小为 <code>&lt;shm size&gt;</code> 共享内存段，其读写权限为指定的 <code>&lt;perm&gt;</code>。</p>
</li>
</ul>
<h2 id="欢迎订阅-进程间通信专栏httpsblogcsdnnetwhowincategory_12404164html"><strong>欢迎订阅 <a href="https://blog.csdn.net/whowin/category_12404164.html">『进程间通信专栏』</a></strong></h2>
<hr>
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: <a href="mailto:hengch@163.com">hengch@163.com</a></strong></p>
<p><img src="https://whowin.gitee.io/images/qrcode/sponsor-qrcode.png" alt="donation"></p>
<!-- for CSDN
[article01]: https://blog.csdn.net/whowin/article/details/132171311
[article02]: https://blog.csdn.net/whowin/article/details/132171930
[article03]: https://blog.csdn.net/whowin/article/details/132172172
[article04]: https://blog.csdn.net/whowin/article/details/134869490
[article05]: https://blog.csdn.net/whowin/article/details/134869636
[article06]: https://blog.csdn.net/whowin/article/details/134939609
-->
<!-- CSDN
[img01]: https://img-blog.csdnimg.cn/img_convert/d1c4560f98b8198a9edef981e3813092.png
[img02]: https://img-blog.csdnimg.cn/img_convert/443c5ae49af630b5454b9be6ead8cdac.png
[img03]: https://img-blog.csdnimg.cn/img_convert/21343b1c6526ccc33d4c4634fa1a2172.png
[img04]: https://img-blog.csdnimg.cn/img_convert/8df27a8c225a69107fa009b07773fa29.png
[img05]: https://img-blog.csdnimg.cn/img_convert/a7a148a5f01498bdc480873da53e1b6a.gif
-->
    </div>

    <div class="post-copyright">
  <p class="copyright-item">
    <span class="item-title">文章作者</span>
    <span class="item-content">whowin</span>
  </p>
  <p class="copyright-item">
    <span class="item-title">上次更新</span>
    <span class="item-content">
        2023-09-12
        
    </span>
  </p>
  
  
</div>
<footer class="post-footer">
      <div class="post-tags">
          <a href="/tags/linux/">Linux</a>
          <a href="/tags/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1/">进程间通信</a>
          <a href="/tags/ipc/">IPC</a>
          <a href="/tags/posix/">POSIX</a>
          <a href="/tags/%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98/">共享内存</a>
          <a href="/tags/shared-memory/">Shared Memory</a>
          </div>
      <nav class="post-nav">
        <a class="prev" href="/post/blog/ipc/0018-posix-shared-memory/">
            <i class="iconfont icon-left"></i>
            <span class="prev-text nav-default">IPC之八：使用 POSIX 共享内存进行进程间通信的实例</span>
            <span class="prev-text nav-mobile">上一篇</span>
          </a>
        <a class="next" href="/post/blog/ipc/0016-posix-semaphores/">
            <span class="next-text nav-default">IPC之六：使用 POSIX 信号量解决经典的&#39;生产者-消费者问题&#39;</span>
            <span class="next-text nav-mobile">下一篇</span>
            <i class="iconfont icon-right"></i>
          </a>
      </nav>
    </footer>
  </article>
        </div>
        

  <span id="/post/blog/ipc/0017-systemv-shared-memory/" class="leancloud_visitors" data-flag-title="IPC之七：使用 System V 共享内存段进行进程间通信的实例">
		<span class="post-meta-item-text">文章阅读量 </span>
		<span class="leancloud-visitors-count">0</span>
		<p></p>
	  </span>
  <div id="vcomments"></div>
  <script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
  <script src='//unpkg.com/valine/dist/Valine.min.js'></script>
  <script type="text/javascript">
    new Valine({
        el: '#vcomments' ,
        appId: 'OFCGzCfJRUglzOdzrqMGkbTR-gzGzoHsz',
        appKey: 'v7P29kPAEbsmaavaYPNhGhnF',
        notify:  false ,
        verify:  false ,
        avatar:'mm',
        placeholder: '说点什么吧...',
        visitor:  true 
    });
  </script>

  

      </div>
    </main>

    <footer id="footer" class="footer">
      <div class="social-links">
      <a href="mailto:hengch@163.com" class="iconfont icon-email" title="email"></a>
  <a href="https://whowin.gitee.io/index.xml" type="application/rss+xml" class="iconfont icon-rss" title="rss"></a>
</div>
<div class="copyright">
  <span class="power-by">
    由 <a class="hexo-link" href="https://gohugo.io">Hugo</a> 强力驱动
  </span>
  <span class="division">|</span>
  <span class="theme-info">
    主题 - 
    <a class="theme-link" href="https://github.com/olOwOlo/hugo-theme-even">Even</a>
  </span>

  <div class="busuanzi-footer">
    <span id="busuanzi_container_site_pv"> 本站总访问量 <span id="busuanzi_value_site_pv"><img src="/img/spinner.svg" alt="spinner.svg"/></span> 次 </span>
      <span class="division">|</span>
    <span id="busuanzi_container_site_uv"> 本站总访客数 <span id="busuanzi_value_site_uv"><img src="/img/spinner.svg" alt="spinner.svg"/></span> 人 </span>
  </div>

  <span class="copyright-year">
    &copy; 
    2022 - 
    2024<span class="heart"><i class="iconfont icon-heart"></i></span><span>whowin</span>
  </span>
</div>

    </footer>

    <div class="back-to-top" id="back-to-top">
      <i class="iconfont icon-up"></i>
    </div>
  </div>
  
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/slideout@1.0.1/dist/slideout.min.js" integrity="sha256-t+zJ/g8/KXIJMjSVQdnibt4dlaDxc9zXr/9oNPeWqdg=" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.1.20/dist/jquery.fancybox.min.js" integrity="sha256-XVLffZaxoWfGUEbdzuLi7pwaUJv1cecsQJQqGLe7axY=" crossorigin="anonymous"></script>



<script type="text/javascript" src="/js/main.min.64437849d125a2d603b3e71d6de5225d641a32d17168a58106e0b61852079683.js"></script>
  <script type="text/javascript">
    window.MathJax = {
      tex: {
        inlineMath: [['$','$'], ['\\(','\\)']],
        }
    };
  </script>
  <script async src="https://cdn.jsdelivr.net/npm/mathjax@3.0.5/es5/tex-mml-chtml.js" integrity="sha256-HGLuEfFcsUJGhvB8cQ8nr0gai9EucOOaIxFw7qxmd+w=" crossorigin="anonymous"></script>








</body>
</html>
